package planfile

import (
	"fmt"
	"io"
	"io/ioutil"

	"google.golang.org/protobuf/proto"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/lang/globalref"
	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/hashicorp/terraform/internal/plans/internal/planproto"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/tfdiags"
	"github.com/hashicorp/terraform/version"
	"github.com/zclconf/go-cty/cty"
)

const tfplanFormatVersion = 3
const tfplanFilename = "tfplan"

// ---------------------------------------------------------------------------
// This file deals with the internal structure of the "tfplan" sub-file within
// the plan file format. It's all private API, wrapped by methods defined
// elsewhere. This is the only file that should import the
// ../internal/planproto package, which contains the ugly stubs generated
// by the protobuf compiler.
// ---------------------------------------------------------------------------

// readTfplan reads a protobuf-encoded description from the plan portion of
// a plan file, which is stored in a special file in the archive called
// "tfplan".
func readTfplan(r io.Reader) (*plans.Plan, error) {
	src, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, err
	}

	var rawPlan planproto.Plan
	err = proto.Unmarshal(src, &rawPlan)
	if err != nil {
		return nil, fmt.Errorf("parse error: %s", err)
	}

	if rawPlan.Version != tfplanFormatVersion {
		return nil, fmt.Errorf("unsupported plan file format version %d; only version %d is supported", rawPlan.Version, tfplanFormatVersion)
	}

	if rawPlan.TerraformVersion != version.String() {
		return nil, fmt.Errorf("plan file was created by Terraform %s, but this is %s; plan files cannot be transferred between different Terraform versions", rawPlan.TerraformVersion, version.String())
	}

	plan := &plans.Plan{
		VariableValues: map[string]plans.DynamicValue{},
		Changes: &plans.Changes{
			Outputs:   []*plans.OutputChangeSrc{},
			Resources: []*plans.ResourceInstanceChangeSrc{},
		},
		DriftedResources: []*plans.ResourceInstanceChangeSrc{},
		Conditions:       make(plans.Conditions),
	}

	switch rawPlan.UiMode {
	case planproto.Mode_NORMAL:
		plan.UIMode = plans.NormalMode
	case planproto.Mode_DESTROY:
		plan.UIMode = plans.DestroyMode
	case planproto.Mode_REFRESH_ONLY:
		plan.UIMode = plans.RefreshOnlyMode
	default:
		return nil, fmt.Errorf("plan has invalid mode %s", rawPlan.UiMode)
	}

	for _, rawOC := range rawPlan.OutputChanges {
		name := rawOC.Name
		change, err := changeFromTfplan(rawOC.Change)
		if err != nil {
			return nil, fmt.Errorf("invalid plan for output %q: %s", name, err)
		}

		plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{
			// All output values saved in the plan file are root module outputs,
			// since we don't retain others. (They can be easily recomputed
			// during apply).
			Addr:      addrs.OutputValue{Name: name}.Absolute(addrs.RootModuleInstance),
			ChangeSrc: *change,
			Sensitive: rawOC.Sensitive,
		})
	}

	for _, rawCR := range rawPlan.ConditionResults {
		conditionAddr := rawCR.ConditionAddr
		cr := &plans.ConditionResult{
			ErrorMessage: rawCR.ErrorMessage,
		}
		switch r := rawCR.Result.(type) {
		case *planproto.ConditionResult_Value:
			cr.Result = cty.BoolVal(r.Value)
		case *planproto.ConditionResult_Unknown:
			cr.Result = cty.UnknownVal(cty.Bool)
		}
		var diags tfdiags.Diagnostics
		switch rawCR.Type {
		case planproto.ConditionType_OUTPUT_PRECONDITION:
			cr.Type = addrs.OutputPrecondition
			cr.Address, diags = addrs.ParseAbsOutputValueStr(rawCR.Addr)
			if diags.HasErrors() {
				return nil, diags.Err()
			}
		case planproto.ConditionType_RESOURCE_PRECONDITION:
			cr.Type = addrs.ResourcePrecondition
			cr.Address, diags = addrs.ParseAbsResourceInstanceStr(rawCR.Addr)
			if diags.HasErrors() {
				return nil, diags.Err()
			}
		case planproto.ConditionType_RESOURCE_POSTCONDITION:
			cr.Type = addrs.ResourcePostcondition
			cr.Address, diags = addrs.ParseAbsResourceInstanceStr(rawCR.Addr)
			if diags.HasErrors() {
				return nil, diags.Err()
			}
		default:
			return nil, fmt.Errorf("condition result %s has unsupported type %s", rawCR.ConditionAddr, rawCR.Type)
		}
		plan.Conditions[conditionAddr] = cr
	}

	for _, rawRC := range rawPlan.ResourceChanges {
		change, err := resourceChangeFromTfplan(rawRC)
		if err != nil {
			// errors from resourceChangeFromTfplan already include context
			return nil, err
		}

		plan.Changes.Resources = append(plan.Changes.Resources, change)
	}

	for _, rawRC := range rawPlan.ResourceDrift {
		change, err := resourceChangeFromTfplan(rawRC)
		if err != nil {
			// errors from resourceChangeFromTfplan already include context
			return nil, err
		}

		plan.DriftedResources = append(plan.DriftedResources, change)
	}

	for _, rawRA := range rawPlan.RelevantAttributes {
		ra, err := resourceAttrFromTfplan(rawRA)
		if err != nil {
			return nil, err
		}
		plan.RelevantAttributes = append(plan.RelevantAttributes, ra)
	}

	for _, rawTargetAddr := range rawPlan.TargetAddrs {
		target, diags := addrs.ParseTargetStr(rawTargetAddr)
		if diags.HasErrors() {
			return nil, fmt.Errorf("plan contains invalid target address %q: %s", target, diags.Err())
		}
		plan.TargetAddrs = append(plan.TargetAddrs, target.Subject)
	}

	for _, rawReplaceAddr := range rawPlan.ForceReplaceAddrs {
		addr, diags := addrs.ParseAbsResourceInstanceStr(rawReplaceAddr)
		if diags.HasErrors() {
			return nil, fmt.Errorf("plan contains invalid force-replace address %q: %s", addr, diags.Err())
		}
		plan.ForceReplaceAddrs = append(plan.ForceReplaceAddrs, addr)
	}

	for name, rawVal := range rawPlan.Variables {
		val, err := valueFromTfplan(rawVal)
		if err != nil {
			return nil, fmt.Errorf("invalid value for input variable %q: %s", name, err)
		}
		plan.VariableValues[name] = val
	}

	if rawBackend := rawPlan.Backend; rawBackend == nil {
		return nil, fmt.Errorf("plan file has no backend settings; backend settings are required")
	} else {
		config, err := valueFromTfplan(rawBackend.Config)
		if err != nil {
			return nil, fmt.Errorf("plan file has invalid backend configuration: %s", err)
		}
		plan.Backend = plans.Backend{
			Type:      rawBackend.Type,
			Config:    config,
			Workspace: rawBackend.Workspace,
		}
	}

	return plan, nil
}

func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) {
	if rawChange == nil {
		// Should never happen in practice, since protobuf can't represent
		// a nil value in a list.
		return nil, fmt.Errorf("resource change object is absent")
	}

	ret := &plans.ResourceInstanceChangeSrc{}

	if rawChange.Addr == "" {
		// If "Addr" isn't populated then seems likely that this is a plan
		// file created by an earlier version of Terraform, which had the
		// same information spread over various other fields:
		// ModulePath, Mode, Name, Type, and InstanceKey.
		return nil, fmt.Errorf("no instance address for resource instance change; perhaps this plan was created by a different version of Terraform?")
	}

	instAddr, diags := addrs.ParseAbsResourceInstanceStr(rawChange.Addr)
	if diags.HasErrors() {
		return nil, fmt.Errorf("invalid resource instance address %q: %w", rawChange.Addr, diags.Err())
	}
	prevRunAddr := instAddr
	if rawChange.PrevRunAddr != "" {
		prevRunAddr, diags = addrs.ParseAbsResourceInstanceStr(rawChange.PrevRunAddr)
		if diags.HasErrors() {
			return nil, fmt.Errorf("invalid resource instance previous run address %q: %w", rawChange.PrevRunAddr, diags.Err())
		}
	}

	providerAddr, diags := addrs.ParseAbsProviderConfigStr(rawChange.Provider)
	if diags.HasErrors() {
		return nil, diags.Err()
	}
	ret.ProviderAddr = providerAddr

	ret.Addr = instAddr
	ret.PrevRunAddr = prevRunAddr

	if rawChange.DeposedKey != "" {
		if len(rawChange.DeposedKey) != 8 {
			return nil, fmt.Errorf("deposed object for %s has invalid deposed key %q", ret.Addr, rawChange.DeposedKey)
		}
		ret.DeposedKey = states.DeposedKey(rawChange.DeposedKey)
	}

	ret.RequiredReplace = cty.NewPathSet()
	for _, p := range rawChange.RequiredReplace {
		path, err := pathFromTfplan(p)
		if err != nil {
			return nil, fmt.Errorf("invalid path in required replace: %s", err)
		}
		ret.RequiredReplace.Add(path)
	}

	change, err := changeFromTfplan(rawChange.Change)
	if err != nil {
		return nil, fmt.Errorf("invalid plan for resource %s: %s", ret.Addr, err)
	}

	ret.ChangeSrc = *change

	switch rawChange.ActionReason {
	case planproto.ResourceInstanceActionReason_NONE:
		ret.ActionReason = plans.ResourceInstanceChangeNoReason
	case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE:
		ret.ActionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate
	case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED:
		ret.ActionReason = plans.ResourceInstanceReplaceBecauseTainted
	case planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST:
		ret.ActionReason = plans.ResourceInstanceReplaceByRequest
	case planproto.ResourceInstanceActionReason_REPLACE_BY_TRIGGERS:
		ret.ActionReason = plans.ResourceInstanceReplaceByTriggers
	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG:
		ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoResourceConfig
	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION:
		ret.ActionReason = plans.ResourceInstanceDeleteBecauseWrongRepetition
	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX:
		ret.ActionReason = plans.ResourceInstanceDeleteBecauseCountIndex
	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY:
		ret.ActionReason = plans.ResourceInstanceDeleteBecauseEachKey
	case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE:
		ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoModule
	case planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN:
		ret.ActionReason = plans.ResourceInstanceReadBecauseConfigUnknown
	case planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING:
		ret.ActionReason = plans.ResourceInstanceReadBecauseDependencyPending
	default:
		return nil, fmt.Errorf("resource has invalid action reason %s", rawChange.ActionReason)
	}

	if len(rawChange.Private) != 0 {
		ret.Private = rawChange.Private
	}

	return ret, nil
}

func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) {
	if rawChange == nil {
		return nil, fmt.Errorf("change object is absent")
	}

	ret := &plans.ChangeSrc{}

	// -1 indicates that there is no index. We'll customize these below
	// depending on the change action, and then decode.
	beforeIdx, afterIdx := -1, -1

	switch rawChange.Action {
	case planproto.Action_NOOP:
		ret.Action = plans.NoOp
		beforeIdx = 0
		afterIdx = 0
	case planproto.Action_CREATE:
		ret.Action = plans.Create
		afterIdx = 0
	case planproto.Action_READ:
		ret.Action = plans.Read
		beforeIdx = 0
		afterIdx = 1
	case planproto.Action_UPDATE:
		ret.Action = plans.Update
		beforeIdx = 0
		afterIdx = 1
	case planproto.Action_DELETE:
		ret.Action = plans.Delete
		beforeIdx = 0
	case planproto.Action_CREATE_THEN_DELETE:
		ret.Action = plans.CreateThenDelete
		beforeIdx = 0
		afterIdx = 1
	case planproto.Action_DELETE_THEN_CREATE:
		ret.Action = plans.DeleteThenCreate
		beforeIdx = 0
		afterIdx = 1
	default:
		return nil, fmt.Errorf("invalid change action %s", rawChange.Action)
	}

	if beforeIdx != -1 {
		if l := len(rawChange.Values); l <= beforeIdx {
			return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action)
		}
		var err error
		ret.Before, err = valueFromTfplan(rawChange.Values[beforeIdx])
		if err != nil {
			return nil, fmt.Errorf("invalid \"before\" value: %s", err)
		}
		if ret.Before == nil {
			return nil, fmt.Errorf("missing \"before\" value: %s", err)
		}
	}
	if afterIdx != -1 {
		if l := len(rawChange.Values); l <= afterIdx {
			return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action)
		}
		var err error
		ret.After, err = valueFromTfplan(rawChange.Values[afterIdx])
		if err != nil {
			return nil, fmt.Errorf("invalid \"after\" value: %s", err)
		}
		if ret.After == nil {
			return nil, fmt.Errorf("missing \"after\" value: %s", err)
		}
	}

	sensitive := cty.NewValueMarks(marks.Sensitive)
	beforeValMarks, err := pathValueMarksFromTfplan(rawChange.BeforeSensitivePaths, sensitive)
	if err != nil {
		return nil, fmt.Errorf("failed to decode before sensitive paths: %s", err)
	}
	afterValMarks, err := pathValueMarksFromTfplan(rawChange.AfterSensitivePaths, sensitive)
	if err != nil {
		return nil, fmt.Errorf("failed to decode after sensitive paths: %s", err)
	}
	if len(beforeValMarks) > 0 {
		ret.BeforeValMarks = beforeValMarks
	}
	if len(afterValMarks) > 0 {
		ret.AfterValMarks = afterValMarks
	}

	return ret, nil
}

func valueFromTfplan(rawV *planproto.DynamicValue) (plans.DynamicValue, error) {
	if len(rawV.Msgpack) == 0 { // len(0) because that's the default value for a "bytes" in protobuf
		return nil, fmt.Errorf("dynamic value does not have msgpack serialization")
	}

	return plans.DynamicValue(rawV.Msgpack), nil
}

// writeTfplan serializes the given plan into the protobuf-based format used
// for the "tfplan" portion of a plan file.
func writeTfplan(plan *plans.Plan, w io.Writer) error {
	if plan == nil {
		return fmt.Errorf("cannot write plan file for nil plan")
	}
	if plan.Changes == nil {
		return fmt.Errorf("cannot write plan file with nil changeset")
	}

	rawPlan := &planproto.Plan{
		Version:          tfplanFormatVersion,
		TerraformVersion: version.String(),

		Variables:        map[string]*planproto.DynamicValue{},
		OutputChanges:    []*planproto.OutputChange{},
		ConditionResults: []*planproto.ConditionResult{},
		ResourceChanges:  []*planproto.ResourceInstanceChange{},
		ResourceDrift:    []*planproto.ResourceInstanceChange{},
	}

	switch plan.UIMode {
	case plans.NormalMode:
		rawPlan.UiMode = planproto.Mode_NORMAL
	case plans.DestroyMode:
		rawPlan.UiMode = planproto.Mode_DESTROY
	case plans.RefreshOnlyMode:
		rawPlan.UiMode = planproto.Mode_REFRESH_ONLY
	default:
		return fmt.Errorf("plan has unsupported mode %s", plan.UIMode)
	}

	for _, oc := range plan.Changes.Outputs {
		// When serializing a plan we only retain the root outputs, since
		// changes to these are externally-visible side effects (e.g. via
		// terraform_remote_state).
		if !oc.Addr.Module.IsRoot() {
			continue
		}

		name := oc.Addr.OutputValue.Name

		// Writing outputs as cty.DynamicPseudoType forces the stored values
		// to also contain dynamic type information, so we can recover the
		// original type when we read the values back in readTFPlan.
		protoChange, err := changeToTfplan(&oc.ChangeSrc)
		if err != nil {
			return fmt.Errorf("cannot write output value %q: %s", name, err)
		}

		rawPlan.OutputChanges = append(rawPlan.OutputChanges, &planproto.OutputChange{
			Name:      name,
			Change:    protoChange,
			Sensitive: oc.Sensitive,
		})
	}

	for addr, cr := range plan.Conditions {
		pcr := &planproto.ConditionResult{
			Addr:          cr.Address.String(),
			ConditionAddr: addr,
			ErrorMessage:  cr.ErrorMessage,
		}
		if cr.Result.IsKnown() {
			pcr.Result = &planproto.ConditionResult_Value{
				Value: cr.Result.True(),
			}
		} else {
			pcr.Result = &planproto.ConditionResult_Unknown{
				Unknown: true,
			}
		}
		switch cr.Type {
		case addrs.OutputPrecondition:
			pcr.Type = planproto.ConditionType_OUTPUT_PRECONDITION
		case addrs.ResourcePrecondition:
			pcr.Type = planproto.ConditionType_RESOURCE_PRECONDITION
		case addrs.ResourcePostcondition:
			pcr.Type = planproto.ConditionType_RESOURCE_POSTCONDITION
		default:
			return fmt.Errorf("condition result %s has unsupported type %s", addr, cr.Type)
		}

		rawPlan.ConditionResults = append(rawPlan.ConditionResults, pcr)
	}

	for _, rc := range plan.Changes.Resources {
		rawRC, err := resourceChangeToTfplan(rc)
		if err != nil {
			return err
		}
		rawPlan.ResourceChanges = append(rawPlan.ResourceChanges, rawRC)
	}

	for _, rc := range plan.DriftedResources {
		rawRC, err := resourceChangeToTfplan(rc)
		if err != nil {
			return err
		}
		rawPlan.ResourceDrift = append(rawPlan.ResourceDrift, rawRC)
	}

	for _, ra := range plan.RelevantAttributes {
		rawRA, err := resourceAttrToTfplan(ra)
		if err != nil {
			return err
		}
		rawPlan.RelevantAttributes = append(rawPlan.RelevantAttributes, rawRA)
	}

	for _, targetAddr := range plan.TargetAddrs {
		rawPlan.TargetAddrs = append(rawPlan.TargetAddrs, targetAddr.String())
	}

	for _, replaceAddr := range plan.ForceReplaceAddrs {
		rawPlan.ForceReplaceAddrs = append(rawPlan.ForceReplaceAddrs, replaceAddr.String())
	}

	for name, val := range plan.VariableValues {
		rawPlan.Variables[name] = valueToTfplan(val)
	}

	if plan.Backend.Type == "" || plan.Backend.Config == nil {
		// This suggests a bug in the code that created the plan, since it
		// ought to always have a backend populated, even if it's the default
		// "local" backend with a local state file.
		return fmt.Errorf("plan does not have a backend configuration")
	}

	rawPlan.Backend = &planproto.Backend{
		Type:      plan.Backend.Type,
		Config:    valueToTfplan(plan.Backend.Config),
		Workspace: plan.Backend.Workspace,
	}

	src, err := proto.Marshal(rawPlan)
	if err != nil {
		return fmt.Errorf("serialization error: %s", err)
	}

	_, err = w.Write(src)
	if err != nil {
		return fmt.Errorf("failed to write plan to plan file: %s", err)
	}

	return nil
}

func resourceAttrToTfplan(ra globalref.ResourceAttr) (*planproto.PlanResourceAttr, error) {
	res := &planproto.PlanResourceAttr{}

	res.Resource = ra.Resource.String()
	attr, err := pathToTfplan(ra.Attr)
	if err != nil {
		return res, err
	}
	res.Attr = attr
	return res, nil
}

func resourceAttrFromTfplan(ra *planproto.PlanResourceAttr) (globalref.ResourceAttr, error) {
	var res globalref.ResourceAttr
	if ra.Resource == "" {
		return res, fmt.Errorf("missing resource address from relevant attribute")
	}

	instAddr, diags := addrs.ParseAbsResourceInstanceStr(ra.Resource)
	if diags.HasErrors() {
		return res, fmt.Errorf("invalid resource instance address %q in relevant attributes: %w", ra.Resource, diags.Err())
	}

	res.Resource = instAddr
	path, err := pathFromTfplan(ra.Attr)
	if err != nil {
		return res, fmt.Errorf("invalid path in %q relevant attribute: %s", res.Resource, err)
	}

	res.Attr = path
	return res, nil
}

func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) {
	ret := &planproto.ResourceInstanceChange{}

	if change.PrevRunAddr.Resource.Resource.Type == "" {
		// Suggests that an old caller wasn't yet updated to populate this
		// properly. All code that generates plans should populate this field,
		// even if it's just to write in the same value as in change.Addr.
		change.PrevRunAddr = change.Addr
	}

	ret.Addr = change.Addr.String()
	ret.PrevRunAddr = change.PrevRunAddr.String()
	if ret.PrevRunAddr == ret.Addr {
		// In the on-disk format we leave PrevRunAddr unpopulated in the common
		// case where it's the same as Addr, and then fill it back in again on
		// read.
		ret.PrevRunAddr = ""
	}

	ret.DeposedKey = string(change.DeposedKey)
	ret.Provider = change.ProviderAddr.String()

	requiredReplace := change.RequiredReplace.List()
	ret.RequiredReplace = make([]*planproto.Path, 0, len(requiredReplace))
	for _, p := range requiredReplace {
		path, err := pathToTfplan(p)
		if err != nil {
			return nil, fmt.Errorf("invalid path in required replace: %s", err)
		}
		ret.RequiredReplace = append(ret.RequiredReplace, path)
	}

	valChange, err := changeToTfplan(&change.ChangeSrc)
	if err != nil {
		return nil, fmt.Errorf("failed to serialize resource %s change: %s", change.Addr, err)
	}
	ret.Change = valChange

	switch change.ActionReason {
	case plans.ResourceInstanceChangeNoReason:
		ret.ActionReason = planproto.ResourceInstanceActionReason_NONE
	case plans.ResourceInstanceReplaceBecauseCannotUpdate:
		ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE
	case plans.ResourceInstanceReplaceBecauseTainted:
		ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED
	case plans.ResourceInstanceReplaceByRequest:
		ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST
	case plans.ResourceInstanceReplaceByTriggers:
		ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_TRIGGERS
	case plans.ResourceInstanceDeleteBecauseNoResourceConfig:
		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG
	case plans.ResourceInstanceDeleteBecauseWrongRepetition:
		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION
	case plans.ResourceInstanceDeleteBecauseCountIndex:
		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX
	case plans.ResourceInstanceDeleteBecauseEachKey:
		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY
	case plans.ResourceInstanceDeleteBecauseNoModule:
		ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE
	case plans.ResourceInstanceReadBecauseConfigUnknown:
		ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN
	case plans.ResourceInstanceReadBecauseDependencyPending:
		ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING
	default:
		return nil, fmt.Errorf("resource %s has unsupported action reason %s", change.Addr, change.ActionReason)
	}

	if len(change.Private) > 0 {
		ret.Private = change.Private
	}

	return ret, nil
}

func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) {
	ret := &planproto.Change{}

	before := valueToTfplan(change.Before)
	after := valueToTfplan(change.After)

	beforeSensitivePaths, err := pathValueMarksToTfplan(change.BeforeValMarks)
	if err != nil {
		return nil, err
	}
	afterSensitivePaths, err := pathValueMarksToTfplan(change.AfterValMarks)
	if err != nil {
		return nil, err
	}
	ret.BeforeSensitivePaths = beforeSensitivePaths
	ret.AfterSensitivePaths = afterSensitivePaths

	switch change.Action {
	case plans.NoOp:
		ret.Action = planproto.Action_NOOP
		ret.Values = []*planproto.DynamicValue{before} // before and after should be identical
	case plans.Create:
		ret.Action = planproto.Action_CREATE
		ret.Values = []*planproto.DynamicValue{after}
	case plans.Read:
		ret.Action = planproto.Action_READ
		ret.Values = []*planproto.DynamicValue{before, after}
	case plans.Update:
		ret.Action = planproto.Action_UPDATE
		ret.Values = []*planproto.DynamicValue{before, after}
	case plans.Delete:
		ret.Action = planproto.Action_DELETE
		ret.Values = []*planproto.DynamicValue{before}
	case plans.DeleteThenCreate:
		ret.Action = planproto.Action_DELETE_THEN_CREATE
		ret.Values = []*planproto.DynamicValue{before, after}
	case plans.CreateThenDelete:
		ret.Action = planproto.Action_CREATE_THEN_DELETE
		ret.Values = []*planproto.DynamicValue{before, after}
	default:
		return nil, fmt.Errorf("invalid change action %s", change.Action)
	}

	return ret, nil
}

func valueToTfplan(val plans.DynamicValue) *planproto.DynamicValue {
	if val == nil {
		// protobuf can't represent nil, so we'll represent it as a
		// DynamicValue that has no serializations at all.
		return &planproto.DynamicValue{}
	}
	return &planproto.DynamicValue{
		Msgpack: []byte(val),
	}
}

func pathValueMarksFromTfplan(paths []*planproto.Path, marks cty.ValueMarks) ([]cty.PathValueMarks, error) {
	ret := make([]cty.PathValueMarks, 0, len(paths))
	for _, p := range paths {
		path, err := pathFromTfplan(p)
		if err != nil {
			return nil, err
		}
		ret = append(ret, cty.PathValueMarks{
			Path:  path,
			Marks: marks,
		})
	}
	return ret, nil
}

func pathValueMarksToTfplan(pvm []cty.PathValueMarks) ([]*planproto.Path, error) {
	ret := make([]*planproto.Path, 0, len(pvm))
	for _, p := range pvm {
		path, err := pathToTfplan(p.Path)
		if err != nil {
			return nil, err
		}
		ret = append(ret, path)
	}
	return ret, nil
}

func pathFromTfplan(path *planproto.Path) (cty.Path, error) {
	ret := make([]cty.PathStep, 0, len(path.Steps))
	for _, step := range path.Steps {
		switch s := step.Selector.(type) {
		case *planproto.Path_Step_ElementKey:
			dynamicVal, err := valueFromTfplan(s.ElementKey)
			if err != nil {
				return nil, fmt.Errorf("error decoding path index step: %s", err)
			}
			ty, err := dynamicVal.ImpliedType()
			if err != nil {
				return nil, fmt.Errorf("error determining path index type: %s", err)
			}
			val, err := dynamicVal.Decode(ty)
			if err != nil {
				return nil, fmt.Errorf("error decoding path index value: %s", err)
			}
			ret = append(ret, cty.IndexStep{Key: val})
		case *planproto.Path_Step_AttributeName:
			ret = append(ret, cty.GetAttrStep{Name: s.AttributeName})
		default:
			return nil, fmt.Errorf("Unsupported path step %t", step.Selector)
		}
	}
	return ret, nil
}

func pathToTfplan(path cty.Path) (*planproto.Path, error) {
	steps := make([]*planproto.Path_Step, 0, len(path))
	for _, step := range path {
		switch s := step.(type) {
		case cty.IndexStep:
			value, err := plans.NewDynamicValue(s.Key, s.Key.Type())
			if err != nil {
				return nil, fmt.Errorf("Error encoding path step: %s", err)
			}
			steps = append(steps, &planproto.Path_Step{
				Selector: &planproto.Path_Step_ElementKey{
					ElementKey: valueToTfplan(value),
				},
			})
		case cty.GetAttrStep:
			steps = append(steps, &planproto.Path_Step{
				Selector: &planproto.Path_Step_AttributeName{
					AttributeName: s.Name,
				},
			})
		default:
			return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step)
		}
	}
	return &planproto.Path{
		Steps: steps,
	}, nil
}
